package owg.util.opengl;

import java.awt.Dimension;
import java.awt.Rectangle;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.util.LinkedList;
import java.util.Queue;
import java.util.WeakHashMap;

import com.jogamp.opengl.GL;
import com.jogamp.opengl.GL2;
import com.jogamp.opengl.GL3;
import com.jogamp.opengl.GLContext;
import com.jogamp.opengl.GLException;
import com.jogamp.opengl.glu.GLU;

import owg.util.Calc;
import owg.util.euclidian.MatrixStack;
import owg.util.euclidian.V3F;
import owg.util.opengl.models.HPRF;


/**This abstract class defines abstractions for basic operations that require the GL.*/
public abstract class EGL<GLT extends GL> {
	public static final int DEFAULT_DEPTH_FUNC = GL2.GL_LEQUAL;
	
	/**Maps GL instances to EGL instances. This makes it possible to recover the EGL through {@link GLContext#getCurrentGL()}*/
	private static final WeakHashMap<GL, EGL<?>> egls = new WeakHashMap<>();
	/**Register an EGL instance.*/
	private static synchronized void register(GL gl, EGL<?> egl)
	{
		egls.put(gl.getRootGL(), egl);
	}
	/**Returns the EGL instance associated with the given GL. Note that this will return null until the EGL instance has been constructed!<br>
	 * This may be used in conjunction with {@link GLContext#getCurrentGL()}.*/
	public static synchronized EGL<?> getEGL(GL gl)
	{
		return egls.get(gl.getRootGL());
	}
	
	/**Reference to a GLU instance, used to access auxilliary OpenGL functions not part of the spec.*/
	public final GLU glu;
	
	/**Reusable renderable primitive. Fills the space between 0 and 1 on each relevant axis.<br/>
	 * 1-Dimensional objects will exist on the x axis while 2-Dimensional objects will exist on the XY-plane.*/
	public final HPRF nwSquare;
	/**Reusable renderable primitive. Fills the space between -1 and 1 on each relevant axis.<br/>
	 * 1-Dimensional objects will exist on the x axis while 2-Dimensional objects will exist on the XY-plane.*/
	public final HPRF unitSquare;
	
	/**Queue for enqueuing runnables for execution on the next step.*/
	protected final Queue<Runnable> runnableQueue;
	/**The GL instance.*/
	protected GLT gl;
	
	public EGL(GLT gl, boolean forcePOTS, boolean vSync) 
	{
		register(gl, this);
		if(gl == null)
			throw new GLException("Cannot instantiate EGL with null GL!");
		this.gl = gl;
		this.glu = new GLU();
		this.runnableQueue = new LinkedList<Runnable>();
		
		gl.setSwapInterval(vSync?1:0);
		
		nwSquare = PrimitiveFactory.genSquare(this, 0, 0, 1, 1, 1, 1);
		
		unitSquare = PrimitiveFactory.genSquare(this,  -1, -1,  	1, 1,  		1 ,1);
	}
	
	public void dispose()
	{
		
	}
	
	/**Get the GL instance. The implementation must further specify what kind of GL this is.*/
	public abstract GLT getGL();
	/**Check for OpenGL errors. If an error is encountered, the message is printed along with it.
	 * @return Whether an error was found.*/
	public abstract boolean checkError(String message);
	/**Enqueue an operation to be executed on the next step. <br/>
	 * Useful for doing this from other threads where the GL context is not current.*/
	public final void invokeLater(Runnable r) {
		synchronized(runnableQueue) {
			runnableQueue.offer(r);
		}
	}
	
	public abstract void setCullFaceEnabled(boolean b);
	public abstract void swapCullFace();
	
	
	/**Should be called every step on the thread that owns the GL context. <br/>
	 * Will execute default behaviour and dispatch a call to {@link #stepImpl()}*/
	public final void step() {
		synchronized(runnableQueue) {
			while(!runnableQueue.isEmpty()) {
				Runnable r = runnableQueue.poll();
				r.run();
			}
		}
		stepImpl();
	}
	
	
	/**Perform routine operations on every game step.*/
	protected abstract void stepImpl();
	
	public abstract HPRF genModel(String name, boolean vDynamicDraw, boolean iDynamicDraw, int faceMode, 
		int nVertexComps, boolean useNormal, int nColorComps, int nTexCoords, int nTexCoords2);
	
	/**
	 * Given a texture which contains a rectangle that has been deformed with perspective, this method
	 * can be used to perform the inverse transform and draw a flat rectangle back to the screen.
	 * 
	 * The texture coordinates are computed from the deformed points divided by the total texture size,
	 * and the perspective deformation is performed by computing the inverse w coordinate for the deformation. 
	 * 
	 * @see "Nathan Reed"
	 * 
	 * @param deformation The deformed 2D points of the source rectangle, in counter-clockwise order.
	 * The upper right corner is the first point.
	 * @param textureSize The width and height of the texture
	 * @param drawSize The width and height of the rectangle to draw 
	 * @param normal the surface normal.
	 */
	public void renderUndeformed(V3F[] deformation, Dimension textureSize, Dimension drawSize, V3F normal)
	{
		V3F[] df = deformation;
		Dimension ts = textureSize;
		//Perspective transform(using distances to the crossing of diagonals) method coutesy of Nathan Reed
		float cX,cY;
		{
			//Here, we calculate the crossing point(cX, cY) of the quad's diagonals.
			float x0=df[1].x(), y1=df[1].y();
			float x1=df[3].x(), y2=df[3].y();
			float x2=df[2].x(), y3=df[2].y();
			float x3=df[0].x(), y4=df[0].y();
			
			float div = ((x0-x1)*(y3-y4)-(y1-y2)*(x2-x3));
			if(div==0)
			{cX = (x0+x1+x2+x3)/4;
			cY = (y1+y2+y3+y4)/4;}
			else
			{cX = ((x0*y2-y1*x1)*(x2-x3)-(x0-x1)*(x2*y4-y3*x3))/div;
			cY = ((x0*y2-y1*x1)*(y3-y4)-(y1-y2)*(x2*y4-y3*x3))/div;}
		}
		//Here, we calculate the distances from the diagonal crossing to the points
		float d0 = (float) Calc.dist(cX, cY, df[0].x(), df[0].y());
		float d1 = (float) Calc.dist(cX, cY, df[1].x(), df[1].y());
		float d2 = (float) Calc.dist(cX, cY, df[2].x(), df[2].y());
		float d3 = (float) Calc.dist(cX, cY, df[3].x(), df[3].y());
		//Here, we calculate the q texture coordinate for all points.
		float p0 = 1f/((d0+d2)/d2);
		float p1 = 1f/((d1+d3)/d3);
		float p2 = 1f/((d2+d0)/d0);
		float p3 = 1f/((d3+d1)/d1);
		
		
		immediateMode().glBegin(false, GL.GL_TRIANGLE_STRIP, 2, true, 0, 4);
		immediateMode().glNormal3d(normal.x(), normal.y(), normal.z());
		immediateMode().glTexCoord4f(p1*df[1].x()/ts.width, p1*df[1].y()/ts.height, 0, p1);
		immediateMode().glVertex2f(0,0);
		immediateMode().glTexCoord4f(p2*df[2].x()/ts.width, p2*df[2].y()/ts.height, 0, p2);
		immediateMode().glVertex2f(0,drawSize.height);
		immediateMode().glTexCoord4f(p0*df[0].x()/ts.width, p0*df[0].y()/ts.height, 0, p0);
		immediateMode().glVertex2f(drawSize.width,0);
		immediateMode().glTexCoord4f(p3*df[3].x()/ts.width, p3*df[3].y()/ts.height, 0, p3);
		immediateMode().glVertex2f(drawSize.width,drawSize.height);
		immediateMode().glEnd();
	}
	
	/**
	 * Given a texture, this method can be used to draw a flat quadrilateral with perspective deformation,
	 * in such a manner that the texture mapping is uniform across the two triangles.
	 * 
	 * The texture coordinates are computed from the deformed points divided by the total texture size,
	 * and the perspective deformation is performed by computing the inverse w coordinate for the deformation. 
	 * 
	 * @see "Nathan Reed"
	 * 
	 * @param deformation The deformed 2D points of the destination quad, in counter-clockwise order.
	 * The upper right corner is the first point.
	 * @param textureSize The width and height of the texture
	 * @param drawSize The width and height of the rectangle to draw 
	 * @param normal the surface normal.
	 */
	public void renderDeformed(V3F[] deformation, Dimension textureSize, Dimension drawSize, V3F normal)
	{
		V3F[] df = deformation;
		
		float tX = (float)drawSize.width/textureSize.width;
		float tY = (float)drawSize.height/textureSize.height;
		//Perspective transform(using distances to the crossing of diagonals) method coutesy of Nathan Reed
		float cX,cY;
		{
			//Here, we calculate the crossing point(cX, cY) of the quad's diagonals.
			float x0=df[1].x(), y1=df[1].y();
			float x1=df[3].x(), y2=df[3].y();
			float x2=df[2].x(), y3=df[2].y();
			float x3=df[0].x(), y4=df[0].y();
			
			float div = ((x0-x1)*(y3-y4)-(y1-y2)*(x2-x3));
			if(div==0)
			{cX = (x0+x1+x2+x3)/4;
			cY = (y1+y2+y3+y4)/4;}
			else
			{cX = ((x0*y2-y1*x1)*(x2-x3)-(x0-x1)*(x2*y4-y3*x3))/div;
			cY = ((x0*y2-y1*x1)*(y3-y4)-(y1-y2)*(x2*y4-y3*x3))/div;}
		}
		//Here, we calculate the distances from the diagonal crossing to the points
		float d0 = (float) Calc.dist(cX, cY, df[0].x(), df[0].y());
		float d1 = (float) Calc.dist(cX, cY, df[1].x(), df[1].y());
		float d2 = (float) Calc.dist(cX, cY, df[2].x(), df[2].y());
		float d3 = (float) Calc.dist(cX, cY, df[3].x(), df[3].y());
		//Here, we calculate the q texture coordinate for all points.
		float p0 = (d0+d2)/d2;
		float p1 = (d1+d3)/d3;
		float p2 = (d2+d0)/d0;
		float p3 = (d3+d1)/d1;
		
		immediateMode().glBegin(false, GL.GL_TRIANGLE_STRIP, 2, true, 0, 4);
		immediateMode().glNormal3d(normal.x(), normal.y(), normal.z());
		immediateMode().glTexCoord4f(0, 0, 0, p1);
		immediateMode().glVertex2fv(df[1].p,0);
		immediateMode().glTexCoord4f(0, p2*tY, 0, p2);
		immediateMode().glVertex2fv(df[2].p,0);
		immediateMode().glTexCoord4f(p0*tX, 0, 0, p0);
		immediateMode().glVertex2fv(df[0].p,0);
		immediateMode().glTexCoord4f(p3*tX, p3*tY, 0, p3);
		immediateMode().glVertex2fv(df[3].p, 0);
		immediateMode().glEnd();
	}
	
	
	
	/**Return a buffer containing the data from the array.<br/>
	 * The returned buffer must be usable by OpenGL for rendering, 
	 * so a direct buffer may be required if DMA will be used.*/
	public abstract FloatBuffer asBuffer(float[] data);
	/**Return a buffer containing the data from the array.<br/>
	 * The returned buffer must be usable by OpenGL for rendering, 
	 * so a direct buffer may be required if DMA will be used.*/
	public abstract IntBuffer asBuffer(int[] data);
	/**Return a buffer containing the data from the array.<br/>
	 * The returned buffer must be usable by OpenGL for rendering, 
	 * so a direct buffer may be required if DMA will be used.*/
	public abstract ShortBuffer asBuffer(short[] data);
	/**Return a buffer containing the data from the array.<br/>
	 * The returned buffer must be usable by OpenGL for rendering, 
	 * so a direct buffer may be required if DMA will be used.*/
	public abstract ByteBuffer asBuffer(byte[] data);
	
	/**Clear the entire canvas with the indicated color.*/
	public void clearScreen(ColorF color)
	{
		gl.glClearDepth(1.0f);
		gl.glClearColor(color.get(0), color.get(1), color.get(2), color.get(3));
		gl.glClear(GL3.GL_COLOR_BUFFER_BIT|GL2.GL_DEPTH_BUFFER_BIT);
	}
	/**Set the current color used for drawing.<br/>
	 * Normally, texture color is multiplied by this value, or,
	 * if there is no texture, the output color is set to this value.
	 * This color should not be premultiplied.*/
	public abstract void setColor(ColorF colorF);
	/**Set the RGB components of the current color used for drawing.<br/>
	 * Normally, texture color is multiplied by this value, or,
	 * if there is no texture, the output color is set to this value.
	 * This method will leave the current alpha intact.
	 * This color should not be premultiplied.*/
	public abstract void setRGB(ColorF colorF);
	/**Set the alpha component of the current color used for drawing.*/
	public abstract void setAlpha(float alpha);
	
	abstract boolean shouldPremultiplyColor();
	
	/**Provides a very simple interface for rendering geometry with low complexity.*/
	public abstract ImmediateModeWrapper<GLT> immediateMode();
	
	/**Returns a matrix stack for the traditional modelview matrix.<br/>
	 * If applicable, calling this method will set the current matrix mode to operate on the modelview matrix.<br/>
	 * In practice, this means that, as a general contract, 
	 * references to the object returned <i>previous</i> calls to this method should never be reused.*/
	public abstract MatrixStack modelviewMatrix();
	/**Returns a matrix stack for the traditional texture matrix.<br/>
	 * If applicable, calling this method will set the current matrix mode to operate on the texture matrix.<br/>
	 * In practice, this means that, as a general contract, 
	 * references to the object returned <i>previous</i> calls to this method should never be reused.*/
	public abstract MatrixStack textureMatrix();
	/**Returns a matrix stack for the traditional projection matrix.<br/>
	 * If applicable, calling this method will set the current matrix mode to operate on the projection matrix.<br/>
	 * In practice, this means that, as a general contract, 
	 * references to the object returned <i>previous</i> calls to this method should never be reused.*/
	public abstract MatrixStack projectionMatrix();
	
	/**Sets the current blend mode on the GL.*/
	public abstract void setBlendMode(BlendMode blendMode);

	/**Enable a 2D texture on the GL.*/
	public abstract void enableTexture2D(Tex tex, int subImage);
	/**Enable a 2D texture on the GL.*/
	public abstract void enableTexture2D(int texture, int width, int height);
	/**Disable 2D textures on the GL.*/
	public abstract void disableTexture2D();
	
	
	public void setLineSmoothing(boolean b) 
	{
		if(b)
			gl.glEnable(GL.GL_LINE_SMOOTH);
		else
			gl.glDisable(GL.GL_LINE_SMOOTH);
	}
	public void setLineWidth(float w) 
	{
		gl.glLineWidth(w);
	}
	
	public abstract void setViewport(int x, int y, int w, int h);
	
	public abstract Rectangle getViewport();
	
	public abstract void setScissor(int x, int y, int w, int h) ;
	
	public abstract boolean isScissorEnabled();
	
	public abstract Rectangle getScissor();
	
	public abstract void setScissorEnabled(boolean b);
	
	public abstract void setStippleEnabled(boolean b);

	public abstract void setDisabledFilter(boolean b);
	
	public GLU getGLU()
	{
		return glu;
	}
	public abstract boolean getPreferVBO();
	
	/**Push the current non-matrix state*/
	public abstract void pushAttribs();
	/**Pop the current non-matrix state*/
	public abstract void popAttribs();
	
	/**Returns an appropriate internal format for intensity textures, e.g. GL_RED or GL_INTENSITY depending on implementation.*/
	public abstract int luminanceFormat();
	/**Returns an appropriate internal format for alpha textures, e.g. GL_RED or GL_ALPHA depending on implementation.*/
	public abstract int alphaFormat();
}
